/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.api.runtime.test.model.impl;

import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.Currency;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.activiti.api.process.model.IntegrationContext;
import org.activiti.api.runtime.model.impl.IntegrationContextImpl;
import org.activiti.api.runtime.model.impl.ProcessVariablesMap;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.annotation.Bean;

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
class IntegrationContextImplTest {

    @Autowired
    private ObjectMapper objectMapper;

    private static Instant instant = Instant.now();

    private static Arguments[] testValues = {
        Arguments.of(BigDecimal.valueOf(1000, 2), BigDecimal.valueOf(1000, 2)),
        Arguments.of(Long.valueOf(100000000000L), Long.valueOf(100000000000L)),
        Arguments.of(Integer.valueOf(123), Integer.valueOf(123)),
        Arguments.of(String.valueOf("string"), String.valueOf("string")),
        Arguments.of(String.valueOf("item1,item2"), String.valueOf("item1,item2")),
        Arguments.of(Boolean.valueOf(true), Boolean.valueOf(true)),
        Arguments.of('A', 'A'),
        Arguments.of(Character.valueOf('A'), Character.valueOf('A')),
        Arguments.of(Double.valueOf(123.123), Double.valueOf(123.123)),
        Arguments.of(Float.valueOf(123.123F), Float.valueOf(123.123F)),
        Arguments.of(Byte.valueOf("1"), Byte.valueOf("1")),
        Arguments.of(Short.valueOf("1"), Short.valueOf("1")),
        Arguments.of(Float.valueOf(123.123F), Float.valueOf(123.123F)),
        Arguments.of(100000000000L, 100000000000L),
        Arguments.of(123, 123),
        Arguments.of(true, true),
        Arguments.of(123.123, 123.123),
        Arguments.of(123.123f, 123.123f),
        Arguments.of(null, null),
        Arguments.of(Currency.getInstance("USD"), "USD"),
        Arguments.of(Date.from(instant), Date.from(instant)),
        Arguments.of(LocalDate.ofInstant(instant, ZoneOffset.UTC), LocalDate.ofInstant(instant, ZoneOffset.UTC)),
        Arguments.of(
            LocalDateTime.ofInstant(instant, ZoneOffset.UTC),
            LocalDateTime.ofInstant(instant, ZoneOffset.UTC)
        ),
        Arguments.of(singletonList("item"), singletonList("item")),
        Arguments.of(singletonList(singletonMap("key", "value")), singletonList(singletonMap("key", "value"))),
        Arguments.of(singleton(singletonMap("key", "value")), singleton(singletonMap("key", "value"))),
        Arguments.of(singleton("item"), singleton("item")),
        Arguments.of(singletonMap("key", "value"), singletonMap("key", "value")),
        Arguments.of(
            JsonNodeFactory.instance.objectNode().set("key", TextNode.valueOf("value")),
            JsonNodeFactory.instance.objectNode().set("key", TextNode.valueOf("value"))
        ),
        Arguments.of(
            new CustomPojo("field1", "field2"),
            new LinkedHashMap<String, String>() {
                {
                    put("field1", "field1");
                    put("field2", "field2");
                }
            }
        ),
        Arguments.of(
            new CustomPojoAnnotated("field1", "field2"),
            new LinkedHashMap<String, String>() {
                {
                    put("@class", "org.activiti.api.runtime.test.model.impl.CustomPojoAnnotated");
                    put("field1", "field1");
                    put("field2", "field2");
                }
            }
        ),
    };

    @SpringBootApplication
    static class Application {

        @Bean
        public ObjectMapper objectMapper(Module customizeProcessModelObjectMapper) {
            return new ObjectMapper().registerModule(customizeProcessModelObjectMapper);
        }
    }

    private static Stream<Arguments> testIntegrationContextInBoundVariables() {
        return Stream.of(testValues);
    }

    @ParameterizedTest
    @MethodSource("testIntegrationContextInBoundVariables")
    public void testIntegrationContextInBoundVariables(Object input, Object output) throws IOException {
        // given
        IntegrationContextImpl source = new IntegrationContextImpl();

        source.addInBoundVariable("variable", input);
        // when
        IntegrationContext target = exchangeIntegrationContext(source);

        // then
        assertThat(target.getInBoundVariables()).containsEntry("variable", output);
    }

    private static Stream<Arguments> testIntegrationContextOutBoundVariables() {
        return Stream.of(testValues);
    }

    @ParameterizedTest
    @MethodSource("testIntegrationContextOutBoundVariables")
    public void testIntegrationContextOutBoundVariables(Object input, Object output) throws IOException {
        // given
        IntegrationContextImpl source = new IntegrationContextImpl();

        source.addOutBoundVariable("variable", input);

        // when
        IntegrationContext target = exchangeIntegrationContext(source);

        // then
        assertThat(target.getOutBoundVariables()).containsEntry("variable", output);
    }

    @Test
    public void testProcessVariablesMapDeserializerShouldFallbackToKeyValueMap() throws JsonProcessingException {
        // given
        Map<String, Object> map = new LinkedHashMap<>();

        map.put("age", 123);
        map.put("name", "John");
        map.put("amount", 12.34);
        map.put("bool", true);
        map.put("nullable", null);
        map.put("map", Collections.singletonMap("key", "value"));
        map.put("list", Collections.singletonList("item"));
        map.put("pojo", new CustomPojo("field1", "field2"));

        String json = objectMapper.writeValueAsString(map);

        // when
        ProcessVariablesMap<String, Object> result = objectMapper.readValue(
            json,
            new TypeReference<ProcessVariablesMap<String, Object>>() {}
        );

        // then
        assertThat(result)
            .containsEntry("age", 123)
            .containsEntry("name", "John")
            .containsEntry("bool", true)
            .containsEntry("nullable", null)
            .containsEntry("amount", 12.34)
            .containsEntry("map", Collections.singletonMap("key", "value"))
            .containsEntry("list", Collections.singletonList("item"))
            .containsEntry(
                "pojo",
                new LinkedHashMap<String, String>() {
                    {
                        put("field1", "field1");
                        put("field2", "field2");
                    }
                }
            );
    }

    private IntegrationContext exchangeIntegrationContext(IntegrationContext source) throws IOException {
        return objectMapper.readValue(objectMapper.writeValueAsString(source), IntegrationContext.class);
    }

    @Test
    void assertThatIntegrationContextImplDoesNotAddVariableValuesIntoToStringMethod(){
        //given
        IntegrationContextImpl integrationContext = new IntegrationContextImpl();
        integrationContext.addInBoundVariable("inbound_key1", "inbound_value");
        integrationContext.addInBoundVariable("inbound_key2", "inbound_value");
        integrationContext.addOutBoundVariable("outbound_key1", "outbound_value");
        integrationContext.addOutBoundVariable("outbound_key2", "outbound_value");

        //when
        String toString = integrationContext.toString();

        //then
        assertThat(toString).doesNotContain("inbound_value");
        assertThat(toString).doesNotContain("outbound_value");
        assertThat(
            toString.contains("[inbound_key1, inbound_key2]") ||
                toString.contains("[inbound_key2, inbound_key1]")
        ).isTrue();
        assertThat(
            toString.contains("[outbound_key1, outbound_key2]") ||
                toString.contains("[outbound_key2, outbound_key1]")
        ).isTrue();
    }

    @Test
    void assertThatIntegrationContextImplDoesAddEmptyVariablePartIntoToStringMethod(){
        //given
        IntegrationContextImpl integrationContext = new IntegrationContextImpl();

        //when
        String toString = integrationContext.toString();

        //then
        assertThat(toString).contains("inboundVariablesKeys=[]");
        assertThat(toString).contains("outBoundVariableKeys=[]");
    }

    @Test
    void should_setEphemeralVariablesProperty() {
        IntegrationContextImpl integrationContext = new IntegrationContextImpl();

        assertThat(integrationContext.hasEphemeralVariables()).isFalse();

        integrationContext.setEphemeralVariables(true);
        assertThat(integrationContext.hasEphemeralVariables()).isTrue();

        integrationContext.setEphemeralVariables(false);
        assertThat(integrationContext.hasEphemeralVariables()).isFalse();

        integrationContext.setEphemeralVariables(null);
        assertThat(integrationContext.hasEphemeralVariables()).isFalse();
    }

    @Test
    void should_copyAllFields_when_copyConstructorIsUsed() {
        IntegrationContextImpl original = new IntegrationContextImpl();
        original.setProcessInstanceId("proc123");
        original.setParentProcessInstanceId("parent456");
        original.setRootProcessInstanceId("root789");
        original.setProcessDefinitionId("defId");
        original.setExecutionId("execId");
        original.setProcessDefinitionKey("defKey");
        original.setProcessDefinitionVersion(2);
        original.setBusinessKey("bizKey");
        original.setClientId("clientId");
        original.setClientName("clientName");
        original.setClientType("clientType");
        original.setAppVersion("1.0.0");
        original.setConnectorType("connectorType");
        original.setEphemeralVariables(true);
        original.addInBoundVariable("inKey", "inValue");
        original.addOutBoundVariable("outKey", "outValue");

        IntegrationContextImpl copy = new IntegrationContextImpl(original);

        assertThat(copy).isNotSameAs(original);
        assertThat(copy.getProcessInstanceId()).isEqualTo(original.getProcessInstanceId());
        assertThat(copy.getParentProcessInstanceId()).isEqualTo(original.getParentProcessInstanceId());
        assertThat(copy.getRootProcessInstanceId()).isEqualTo(original.getRootProcessInstanceId());
        assertThat(copy.getProcessDefinitionId()).isEqualTo(original.getProcessDefinitionId());
        assertThat(copy.getExecutionId()).isEqualTo(original.getExecutionId());
        assertThat(copy.getProcessDefinitionKey()).isEqualTo(original.getProcessDefinitionKey());
        assertThat(copy.getProcessDefinitionVersion()).isEqualTo(original.getProcessDefinitionVersion());
        assertThat(copy.getBusinessKey()).isEqualTo(original.getBusinessKey());
        assertThat(copy.getClientId()).isEqualTo(original.getClientId());
        assertThat(copy.getClientName()).isEqualTo(original.getClientName());
        assertThat(copy.getClientType()).isEqualTo(original.getClientType());
        assertThat(copy.getAppVersion()).isEqualTo(original.getAppVersion());
        assertThat(copy.getConnectorType()).isEqualTo(original.getConnectorType());
        assertThat(copy.hasEphemeralVariables()).isEqualTo(original.hasEphemeralVariables());
        assertThat(copy.getInBoundVariables()).isEqualTo(original.getInBoundVariables());
        assertThat(copy.getOutBoundVariables()).isEqualTo(original.getOutBoundVariables());
        assertThat(copy.getInBoundVariables()).isNotSameAs(original.getInBoundVariables());
        assertThat(copy.getOutBoundVariables()).isNotSameAs(original.getOutBoundVariables());
    }

    @Test
    void should_cleanInboundVariables_when_clearInBoundVariablesIsCalled() {
        IntegrationContextImpl integrationContext = new IntegrationContextImpl();
        integrationContext.addInBoundVariable("key1", "value1");
        integrationContext.addInBoundVariable("key2", "value2");

        assertThat(integrationContext.getInBoundVariables()).hasSize(2);

        integrationContext.clearInBoundVariables();

        assertThat(integrationContext.getInBoundVariables()).isEmpty();
    }

    @Test
    void should_cleanOutBoundVariables_when_clearOutBoundVariablesIsCalled() {
        IntegrationContextImpl integrationContext = new IntegrationContextImpl();
        integrationContext.addOutBoundVariable("key1", "value1");
        integrationContext.addOutBoundVariable("key2", "value2");

        assertThat(integrationContext.getOutBoundVariables()).hasSize(2);

        integrationContext.clearOutBoundVariables();

        assertThat(integrationContext.getOutBoundVariables()).isEmpty();
    }
}
